﻿/*
 * Created by SharpDevelop.
 * User: Corrado Mio
 * Date: 01/06/2012
 * Time: 06:51
 * 
 * Le estensioni pensate ed implementate nelle classi Threads e ThreadPools
 * sono troppo 'intrusive', nel senso che si riferiscono a QUALUNQUE thread
 * dell'applicazione.
 * 
 * Invece, molto piu' intelligente e utilizzabile e' la possibilita' di 
 * partizionare i thread che l'applicazione istanzia, in base necessita' locali, 
 * e non globali.
 * 
 * Questo puo' essere fatto mediante la classe 'ThreadGroup'.
 * Tale classe permette di avere un controllo locale di un certo numero di thread, 
 * ma non solo, permette anche di aggiungere delle funzionalita' non permesse 
 * nell'implementazione globale, e cio'e la possibilita' di utilizzare delle
 * callback quando un Thread parte e termina
 * 
 * 
 * Possibili estensioni
 * --------------------
 * 
 * Oltre alle callback, ci dovrebbero essere dei metodi virtuali, in modo che
 * sia possibile estendere la classe aggiungendone nuove funzionalita'
 * 
 * Dovrebbe essere possibile
 * 
 * - attendere il primo Thread che termina
 * - attendere il termine di tutti i Thread
 * - abortire l'esecuzione di tutti i Thread running.
 * 
 */
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;

using log4net;

namespace CSharpExt.Threading
{
    public class ThreadGroupException : ApplicationException
    {
        #region Constructor
        
        public ThreadGroupException(String format, params Object[] args)
            : base(String.Format(format, args))
        {
           
        }

        #endregion
    }
    
    // ======================================================================
    // ThreadGroup
    // ======================================================================
    
    /// <summary>
    /// La classe ha il compito di mantenere il controllo di un gruppo di thread.
    /// 
    /// I thread possono essere istanziati esplicitamente oppure acquisiti dal 
    /// ThreadPool.
    /// 
    /// Un thread e' presente nel ThreadGroup SOLO mentre e' in esecuzione.
    /// 
    /// Mette a disposizione le seguenti funzionalita':
    /// 
    /// - tiene traccia dei thread in esecuzione
    /// - protegge ogni thread dalle eccezioni non gestite
    /// - aggiunge il supporto a Runnable
    /// - mette a disposizione una callback chiamata in caso di eccezione in un thread
    /// 
    /// Nel caso di utilizzo del ThreadPool
    /// 
    /// - tiene traccia del numero di item in coda
    /// 
    /// Nota: viene forzato un nome ad ogni Thread creato.
    ///     Conviene assegnare SEMPRE un nome ad ogni thread.
    ///     Non e' necessario che il nome sia univoco
    /// 
    /// Nota: e' giusto che il threadpool mantenga traccia dei soli thread in esecuzione?
    ///     Forse no: se il ThreadGroup lo crea, allora ne mantiene il reference!
    ///     Da vedere!
    /// 
    /// Nota: il supporto ad una gerarchia di ThreadGroup e' stato rimosso perche' al
    ///     momento non sembra di qualche utilita' pratica
    /// 
    /// </summary>
    public class ThreadGroup
    {
        #region Private Data

        /// <summary>
        /// Nome del ThreadGroup
        /// </summary>
        private String _name;

        /// <summary>
        /// Elenco dei thread correntemente in esecuzione
        /// </summary>
        private Dictionary<int, Thread> _runningThreads  = new Dictionary<int, Thread>();
        
        /// <summary>
        /// Numero di item in coda in attesa di essere processati dai thread del ThreadPool
        /// </summary>
        private int _waitingUserWorkItems;

        #endregion

        #region Constructor

        /// <summary>
        /// Il ThreadGroup DEVE avere un nome in fase di creazione.
        /// </summary>
        /// <param name="name"></param>
        public ThreadGroup(String name)
        {
            if (String.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");
            
            _name = name;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Nome del ThreadGroup
        /// </summary>
        public String Name
        {
            get { return _name; }
        }
        
        /// <summary>
        /// Numero di item in coda al ThreadPool
        /// </summary>
        public int WaitingUserWorkItems
        {
            get { return _waitingUserWorkItems; }
        }
        
        /// <summary>
        /// Numero di Thread correntemente in esecuzione
        /// </summary>
        public int RunningThreadsCount
        {
            get { return _runningThreads.Count; }
        }
        
        /// <summary>
        /// Elenco dei Thread correntemente in esecuzione
        /// </summary>
        public Thread[] RunningThreads
        {
            get
            {
                lock (_runningThreads)
                {
                    Thread[] threads = new Thread[_runningThreads.Count];
                    _runningThreads.Values.CopyTo(threads, 0);
                    return threads;
                }
            }
        }
        
        /// <summary>
        /// Callback chiamata ogni volta che parte un Thread
        /// </summary>
        public Action<Thread> OnThreadStart;
        
        /// <summary>
        /// Callback chiamata quando un Thread termina. Se il
        /// Thread termina normalmente, il parametro Exception sara'
        /// null
        /// </summary>
        public Action<Thread, Exception> OnThreadEnd;

        #endregion

        #region Create
        
        // -- Action()
        
        public Thread Create(Action callback)
        {
            return Create(null, callback);
        }

        public Thread Create(String n, Action callback)
        {
            n = ThreadUtils.ComposeName(n, Name + ".{0}.{1}", callback);
            
            Thread thread = new Thread(
                () =>
                {
                    Thread self = null;
                    Exception exception = null;
                    try
                    {
                        self = enterThread(this);
                        
                        callback();
                    }
                    catch (ThreadAbortException e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("Thread").Error("Thread Aborted");
                    }
                    catch (Exception e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("Thread").Error(e, e);
                    }
                    finally
                    {
                        exitThread(this, exception);
                    }
                })
            {
                Name = n
            };
            
            return thread;
        }

        
        // -- Action(Object)
        
        public Thread Create(Action<Object> callbackArg)
        {
            return Create(null, callbackArg);
        }

        public Thread Create(String n, Action<Object> callbackArg)
        {
            n = ThreadUtils.ComposeName(n, Name + ".{0}.{1}", callbackArg);
            
            Thread thread = new Thread(
                (arg) =>
                {
                    Thread self = null;
                    Exception exception = null;
                    try
                    {
                        self = enterThread(this);
                        
                        callbackArg(arg);
                    }
                    catch (ThreadAbortException e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("Thread").Error("Thread Aborted");
                    }
                    catch (Exception e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("Thread").Error(e, e);
                    }
                    finally
                    {
                        exitThread(this, exception);
                    }
                })
            {
                Name = n
            };
            
            return thread;
        }
        
        
        // -- Runnable
        
        public Thread Create(Runnable runnable)
        {
            return Create(null, runnable);
        }
        
        public Thread Create(String n, Runnable runnable)
        {
            n = ThreadUtils.ComposeName(n, Name + ".{0}", runnable);
            
            return Create(n, () => { runnable.Run(); });
        }
        
        #endregion
        
        #region InvokeAsync
        
        // -- Action()
        
        public bool InvokeAsync(Action callback)
        {
            return InvokeAsync(null, callback);
        }

        public bool InvokeAsync(String n, Action callback)
        {
            enqueueItem();
            
            return ThreadPool.QueueUserWorkItem(
                (arg) => 
                {
                    Thread self = null;
                    Exception exception = null;
                    try
                    {
                        self = enterThread(this, n);
                        
                        callback();
                    }
                    catch(ThreadAbortException e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("ThreadPools.Thread").ErrorFormat("Thread {0} Aborted", Thread.CurrentThread.Name);
                    }
                    catch(Exception e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("ThreadPools.Thread").Error(e, e);
                    }
                    finally
                    {
                        exitThread(this, exception);
                    }
                });
        }

        
        // -- Action(Object)
        
        public bool InvokeAsync(Action<Object> callbackArg, Object state)
        {
            return InvokeAsync(null, callbackArg, state);
        }

        public bool InvokeAsync(String n, Action<Object> callbackArg, Object state)
        {
            enqueueItem();
            
            return ThreadPool.QueueUserWorkItem(
                (arg) => 
                {
                    Thread self = null;
                    Exception exception = null;
                    try
                    {
                        self = enterThread(this, n);
                        
                        callbackArg(arg);
                    }
                    catch(ThreadAbortException e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("ThreadPools.Thread").ErrorFormat("Thread {0} Aborted", Thread.CurrentThread.Name);
                    }
                    catch(Exception e)
                    {
                        exception = e;
                        
                        if (OnThreadEnd == null)
                            LogManager.GetLogger("ThreadPools.Thread").Error(e, e);
                    }
                    finally
                    {
                        exitThread(this, exception);
                    }
                }, 
                state);
        }
        
        
        // -- Runnable
        
        public bool InvokeAsync(Runnable runnable)
        {
            return InvokeAsync(null, callRunnable, runnable);
        }
        
        public bool InvokeAsync(String n, Runnable runnable)
        {
            return InvokeAsync(n, callRunnable, runnable);
        }
        
        #endregion
        
        #region Destroy
        
        public void Destroy()
        {
            foreach (Thread thread in RunningThreads)
            {
                thread.Abort();
            }

            if (_runningThreads.Count != 0)
                throw new ThreadGroupException("ThreadGroup {0} not Empty: still {1} Threads running", Name, _runningThreads.Count);
        }

        #endregion

        #region Implementation
        
        private static Thread enterThread(ThreadGroup tgroup)
        {
            Thread currentThread = Thread.CurrentThread;
            tgroup.addThread(currentThread);
            return currentThread;
        }
        
        private static void exitThread(ThreadGroup tgroup, Exception e)
        {
            Thread currentThread = Thread.CurrentThread;
            tgroup.removeThread(currentThread, e);
        }
        

        internal ThreadGroup addThread(Thread thread)
        {
            if (OnThreadStart != null)
                try { OnThreadStart(thread); } catch (Exception) { }

            lock (_runningThreads)
            {
                if (_runningThreads.ContainsKey(thread.ManagedThreadId))
                    throw new ThreadGroupException("Duplicate Thread {0} in {1}", thread.ManagedThreadId, Name);

                _runningThreads[thread.ManagedThreadId] = thread;
            }
            
            return this;
        }

        internal void removeThread(Thread thread, Exception e)
        {
            lock (_runningThreads)
            {
                _runningThreads.Remove(thread.ManagedThreadId);
            }

            if (OnThreadEnd != null)
                try { OnThreadEnd(thread, e); } catch (Exception) { }
        }

        
        
        private void enqueueItem()
        {
            Interlocked.Increment(ref _waitingUserWorkItems);
        }
        
        private Thread enterThread(ThreadGroup tgroup, String name)
        {
            if (!String.IsNullOrEmpty(name))
                Thread.CurrentThread.Name = name;
                
            Interlocked.Decrement(ref _waitingUserWorkItems);
            
            return enterThread(tgroup);
        }
        
        internal static void callRunnable(Object arg)
        {
            ((Runnable)arg).Run();
        }
        
        #endregion
    }
}

